// TODO UV
// TODO User Agent 解析
// TODO IP 采集解析
import UserMetricsStore, {
  metricsNameUserVital as metricsName,
  IMetrics,
} from './store';
import BehaviorStore, { behaviorStack } from './behaviorStore';
import { Request } from './request';
const history = window.history;
export type PV = {
  routePath: string;
  duration: number;
  startTime: number;
  endTime?: number;
};
export const afterLoad = (callback: any) => {
  if (document.readyState === 'complete') {
    setTimeout(callback);
  } else {
    window.addEventListener('pageshow', callback, {
      once: true,
      capture: true,
    });
  }
};
export interface PageInformation {
  host: string;
  hostname: string;
  href: string;
  protocol: string;
  origin: string;
  port: string;
  pathname: string;
  search: string;
  hash: string;
  // 网页标题
  title: string;
  // 浏览器的语种 (eg:zh) ; 这里截取前两位，有需要也可以不截取
  language: string;
  // 用户 userAgent 信息
  userAgent?: string;
  // 屏幕宽高 (eg:1920x1080)  屏幕宽高意为整个显示屏的宽高
  winScreen: string;
  // 文档宽高 (eg:1388x937)   文档宽高意为当前页面显示的实际宽高（有的同学喜欢半屏显示）
  docScreen: string;
}

// 派发出新的 Event
// 这个好像用到了高阶函数, 牛蛙
const wr = (type: keyof History) => {
  const orig = history[type];
  return function (this: unknown) {
    const rv = orig.apply(this, arguments);
    const e = new Event(type);
    window.dispatchEvent(e);
    return rv;
  };
};

// 添加 pushState replaceState 事件
export const wrHistory = (): void => {
  history.pushState = wr('pushState');
  history.replaceState = wr('replaceState');
};

// 为 pushState 以及 replaceState 方法添加 Event 事件
export const proxyHistory = (handler: Function): void => {
  // 添加对 replaceState 的监听
  window.addEventListener('replaceState', (e) => handler(e), true);
  // 添加对 pushState 的监听
  // window.addEventListener('pushState', (e) => handler(e), true);
};

export const proxyHash = (handler: Function): void => {
  // 添加对 hashchange 的监听
  // hash 变化除了触发 hashchange ,也会触发 popstate 事件,而且会先触发 popstate 事件，我们可以统一监听 popstate
  // 这里可以考虑是否需要监听 hashchange,或者只监听 hashchange
  // window.addEventListener('hashchange', (e) => handler(e), true);
  // 添加对 popstate 的监听
  // 浏览器回退、前进行为触发的 可以自己判断是否要添加监听
  window.addEventListener('popstate', (e) => handler(e), true);
};

// 这里参考了 谷歌GA 的自定义埋点上报数据维度结构
export interface customAnalyticsData {
  // 事件类别 互动的对象 eg:Video
  eventCategory: string;
  // 事件动作 互动动作方式 eg:play
  eventAction: string;
  // 事件标签 对事件进行分类 eg:
  eventLabel: string;
  // 事件值 与事件相关的数值   eg:180min
  eventValue?: string;
}

export interface httpMetrics {
  method: string;
  url: string | URL;
  body: Document | XMLHttpRequestBodyInit | null | undefined | ReadableStream;
  requestTime: number;
  responseTime: number;
  status: number;
  statusText: string;
  response?: any;
}

// 调用 proxyXmlHttp 即可完成全局监听 XMLHttpRequest
export const proxyXmlHttp = (
  sendHandler: Function | null | undefined,
  loadHandler: Function
) => {
  if (
    'XMLHttpRequest' in window &&
    typeof window.XMLHttpRequest === 'function'
  ) {
    const oXMLHttpRequest = window.XMLHttpRequest;
    if (!(window as any).oXMLHttpRequest) {
      // oXMLHttpRequest 为原生的 XMLHttpRequest，可以用以 SDK 进行数据上报，区分业务
      (window as any).oXMLHttpRequest = oXMLHttpRequest;
    }
    const { open, send } = window.XMLHttpRequest.prototype;
    let metrics_method = new Map();
    window.XMLHttpRequest.prototype.open = function pyopen(...rest: any[]) {
      // 可以放一些请求头,  我也不知道这个能干啥
      metrics_method.set(rest[1], rest[0]);
      return open.call(this, rest[0],rest[1], rest[2], rest[3], rest[4]);
    };
    window.XMLHttpRequest.prototype.send = function pysend(body) {
      this.addEventListener('loadend', () => {
        const { responseURL, status, statusText, response } = this;
        console.log(metrics_method);
        const regx = /ht\w+:\/\/.+?\//;
        const url = (responseURL as string).replace(regx, '/');
        let metrics = {
          method:
            metrics_method.get(url) || '',
          body: body || '',
          url,
          status,
          statusText,
          response,
          responseTime: Date.now(),
        } as httpMetrics;
        if (typeof loadHandler === 'function') loadHandler(metrics);
        // xhr.status 状态码
      });
      // sendHandler 可以在发送 Ajax 请求之前，挂载一些信息，比如 header 请求头
      // setRequestHeader 设置请求header，用来传输关键参数等
      // xhr.setRequestHeader('xxx-id', 'VQVE-QEBQ');
      if (typeof sendHandler === 'function') sendHandler(body);
      return send.call(this, ...arguments);
    };
  }
};
// 调用 proxyFetch 即可完成全局监听 fetch
export const proxyFetch = (
  sendHandler: Function | null | undefined,
  loadHandler: Function
) => {
  if ('fetch' in window && typeof window.fetch === 'function') {
    const oFetch = window.fetch;
    if (!(window as any).oFetch) {
      (window as any).oFetch = oFetch;
    }
    (window as any).fetch = async (input: any, init: RequestInit) => {
      // init 是用户手动传入的 fetch 请求互数据，包括了 method、body、headers，要做统一拦截数据修改，直接改init即可
      if (typeof sendHandler === 'function') sendHandler(init);
      let metrics = {} as httpMetrics;

      metrics.method = init?.method || '';
      metrics.url =
        (input && typeof input !== 'string' ? input?.url : input) || ''; // 请求的url
      metrics.body = init?.body || '';
      metrics.requestTime = new Date().getTime();

      return oFetch.call(window, input, init).then(async (response) => {
        // clone 出一个新的 response,再用其做.text(),避免 body stream already read 问题
        const res = response.clone();
        metrics = {
          ...metrics,
          status: res.status,
          statusText: res.statusText,
          response: await res.text(),
          responseTime: new Date().getTime(),
        };
        if (typeof loadHandler === 'function') loadHandler(metrics);
        return response;
      });
    };
  }
};
export interface OriginInformation {
  referrer: string;
  type: number | string;
}

// 返回 OI 用户来路信息
export const getOriginInfo = (): OriginInformation => {
  return {
    referrer: document.referrer,
    type: window.performance?.navigation.type || '',
  };
};

export default class UserVitals {
  private engineInstance: any;

  // 本地暂存数据在 Map 里 （也可以自己用对象来存储）
  public metrics: UserMetricsStore;

  public breadcrumbs: BehaviorStore;

  public customHandler: Function;

  // 最大行为追踪记录数
  public maxBehaviorRecords: number;

  // 允许捕获click事件的DOM标签 eg:button div img canvas
  clickMountList: Array<string>;

  constructor(engineInstance: any) {
    this.engineInstance = engineInstance;
    this.metrics = new UserMetricsStore();
    // 限制最大行为追踪记录数为 100，真实场景下需要外部传入自定义;
    this.maxBehaviorRecords = 100;
    // 初始化行为追踪记录
    this.breadcrumbs = new BehaviorStore({
      maxBehaviorRecords: this.maxBehaviorRecords,
    });
    // 初始化 用户自定义 事件捕获
    this.customHandler = this.initCustomerHandler();
    // 作为 真实sdk 的时候，需要在初始化时传入与默认值合并;
    this.clickMountList = ['button'].map((x) => x.toLowerCase());
    // 重写事件
    wrHistory();
    // 初始化页面基本信息
    this.initPageInfo();
    // 初始化路由跳转获取
    // this.initRouteChange(); // 不知道有什么作用
    // 初始化用户来路信息获取
    this.initOriginInfo();
    // 初始化 PV 的获取;
    this.initPV();
    // 初始化 click 事件捕获
    this.initClickHandler(this.clickMountList);
    // 初始化 Http 请求事件捕获
    this.initHttpHandler();
    // 单页面时间
    this.initPageTime();
    // 上报策略在后几篇细说
  }

  // 封装用户行为的上报入口
  userSendHandler = (data: IMetrics) => {
    new Request().post('api/monitor/test', data);
  };

  // 获取 PI 页面基本信息
  getPageInfo = (): PageInformation => {
    const {
      host,
      hostname,
      href,
      protocol,
      origin,
      port,
      pathname,
      search,
      hash,
    } = window.location;
    const { width, height } = window.screen;
    const { language, userAgent } = navigator;

    return {
      host,
      hostname,
      href,
      protocol,
      origin,
      port,
      pathname,
      search,
      hash,
      title: document.title,
      language: language.substr(0, 2),
      userAgent,
      winScreen: `${width}x${height}`,
      docScreen: `${
        document.documentElement.clientWidth || document.body.clientWidth
      }x${document.documentElement.clientHeight || document.body.clientHeight}`,
    };
  };

  // 补齐 pathname 和 timestamp 参数
  getExtends = (): { page: string; timestamp: number | string } => {
    return {
      page: this.getPageInfo().pathname,
      timestamp: new Date().getTime(),
    };
  };

  // 初始化用户自定义埋点数据的获取上报
  initCustomerHandler = (): Function => {
    const handler = (options: customAnalyticsData) => {
      // 记录到 UserMetricsStore
      this.metrics.add(metricsName.CDR, options);
      // 自定义埋点的信息一般立即上报
      this.userSendHandler(options);
      // 记录到用户行为记录栈
      this.breadcrumbs.push({
        category: metricsName.CDR,
        data: options,
        ...this.getExtends(),
      });
    };

    return handler;
  };

  // 初始化 PI 页面基本信息的获取以及返回
  initPageInfo = (): void => {
    const info: PageInformation = this.getPageInfo();
    const metrics = info as IMetrics;
    this.metrics.set(metricsName.PI, metrics);
  };

  // 初始化 RCR 路由跳转的获取以及返回
  initRouteChange = (): void => {
    const handler = (e: Event) => {
      // 正常记录
      const metrics = {
        // 跳转的方法 eg:replaceState
        jumpType: e.type,
        // 创建时间
        timestamp: new Date().getTime(),
        // 页面信息
        pageInfo: this.getPageInfo(),
      } as IMetrics;
      // 一般路由跳转的信息不会进行上报，根据业务形态决定；
      this.metrics.add(metricsName.RCR, metrics);
      // 行为记录 不需要携带 pageInfo
      delete metrics.pageInfo;
      // 记录到行为记录追踪
      const behavior = {
        category: metricsName.RCR,
        data: metrics,
        ...this.getExtends(),
      } as behaviorStack;
      this.breadcrumbs.push(behavior);
    };
    proxyHash(handler);
    // 为 pushState 以及 replaceState 方法添加 Evetn 事件
    proxyHistory(handler);
  };

  // 初始化 PV 的获取以及返回
  initPV = (): void => {
    const handler = () => {
      const metrics = {
        // 还有一些标识用户身份的信息，由项目使用方传入，任意拓展 eg:userId
        // 创建时间
        timestamp: new Date().getTime(),
        // 页面信息
        pageInfo: this.getPageInfo(),
        // 用户来路
        originInformation: getOriginInfo(),
      } as IMetrics;
      this.userSendHandler(metrics);
      // 一般来说， PV 可以立即上报
    };
    afterLoad(() => {
      handler();
    });
    proxyHash(handler);
    // 为 pushState 以及 replaceState 方法添加 Evetn 事件
    proxyHistory(handler);
  };

  // 初始化 OI 用户来路的获取以及返回
  initOriginInfo = (): void => {
    const info: OriginInformation = getOriginInfo();
    const metrics = info as IMetrics;
    this.metrics.set(metricsName.OI, metrics);
  };

  // 初始化 CBR 点击事件的获取和返回
  initClickHandler = (mountList: Array<string>): void => {
    const handler = (e: MouseEvent | any) => {
      // 这里是根据 tagName 进行是否需要捕获事件的依据，可以根据自己的需要，额外判断id\class等
      // 先判断浏览器支持 e.path ，从 path 里先取
      let target = e.path?.find((x: Element) =>
        mountList.includes(x.tagName?.toLowerCase())
      );
      // 不支持 path 就再判断 target
      target =
        target ||
        (mountList.includes(e.target.tagName?.toLowerCase())
          ? e.target
          : undefined);
      if (!target) return;
      const metrics = {
        tagInfo: {
          id: target.id,
          classList: Array.from(target.classList),
          tagName: target.tagName,
          text: target.textContent,
        },
        // 创建时间
        timestamp: new Date().getTime(),
        // 页面信息
        pageInfo: this.getPageInfo(),
      } as IMetrics;
      // 除开商城业务外，一般不会特意上报点击行为的数据，都是作为辅助检查错误的数据存在;
      this.metrics.add(metricsName.CBR, metrics);
      // 行为记录 不需要携带 完整的pageInfo
      delete metrics.pageInfo;
      // 记录到行为记录追踪
      const behavior = {
        category: metricsName.CBR,
        data: metrics,
        ...this.getExtends(),
      } as behaviorStack;
      this.breadcrumbs.push(behavior);
    };
    window.addEventListener(
      'click',
      (e) => {
        handler(e);
      },
      true
    );
  };

  // 初始化 http 请求的数据获取和上报
  initHttpHandler = (): void => {
    const loadHandler = (metrics: httpMetrics) => {
      if (metrics.status < 400) {
        // 对于正常请求的 HTTP 请求来说,不需要记录 请求体 和 响应体
        delete metrics.response;
        delete metrics.body;
      }
      // 正常得用户请求也得上报
      this.userSendHandler(metrics);
      // 记录到 UserMetricsStore
      this.metrics.add(metricsName.HT, metrics);
      // 记录到用户行为记录栈
      this.breadcrumbs.push({
        category: metricsName.HT,
        data: metrics,
        ...this.getExtends(),
      });
    };
    proxyXmlHttp(null, loadHandler);
    proxyFetch(null, loadHandler);
  };
  // 初始化PI页面停留时间
  initPageTime = (): void => {
    window.addEventListener('load', () => {
      routeList.push({
        routePath: window.location.pathname,
        duration: 0,
        startTime: Date.now(),
      });
      this.metrics.set(metricsName.PT, routeList);
    });

    this.metrics.set(metricsName.PT, []);
    let routeList: Array<PV> = [];
    let recordNextPage = () => {
      // 记录前一个页面的页面停留时间
      let routeList: Array<PV> = this.metrics.get(metricsName.PT) as Array<PV>;
      const time = new Date().getTime();
      routeList[routeList.length - 1].endTime = time;
      routeList[routeList.length - 1].duration =
        time - routeList[routeList.length - 1].startTime;
      // 推一个新的页面停留记录
      routeList.push({
        routePath: window.location.pathname,
        duration: 0,
        startTime: Date.now(),
      });
    };

    // 关闭浏览器前记录最后的时间并上报
    window.addEventListener('beforeunload', () => {
      let routeList: Array<PV> = this.metrics.get(metricsName.PT) as Array<PV>;
      const time = new Date().getTime();
      routeList[routeList.length - 1].endTime = time;
      routeList[routeList.length - 1].duration =
        time - routeList[routeList.length - 1].startTime;
      this.userSendHandler(routeList)
    });
    proxyHash(recordNextPage);
    proxyHistory(recordNextPage);
  };
}
